Repository navigation
✨ feat(settings): the GitHub API hourly caps per agent can be set from the dashboard and the admin MCP - #17385
Conversation
The API proxy's per-agent GitHub API hourly cap (agents.github_api_hourly_cap
and agents.<name>.github_api_hourly_cap) could only be changed in hive.yaml,
which on a hub-managed spoke only the hub can write. A reviewer that ran out
of its 300 calls overnight had its later reviews come back as false
"requires human" with no way for the spoke owner to raise its cap.
- Settings -> Forge App: a fleet default control beside the agent reserve
floor (number; 0 = no cap; blank = the default 300), saved through
PUT /api/config/github as agent_github_api_hourly_cap (null removes the key).
- Each agent's General tab: its own cap (number; 0 = no cap; blank = inherit),
saved through PUT /api/config/agent/{name}/general as githubApiHourlyCap.
A replica or review head shows its base agent's cap read-only and the
endpoint refuses an override on it.
- Both write the live config the proxy reads (main.go binds the cap getter to
the same *Config), so a save applies on the next proxied request with no
restart; both save to hive.yaml and the dashboard overlay and audit the old
and new value.
- Admin MCP: agent.github_api_hourly_cap and github.agent_api_hourly_cap
write ops. The preview reads the current value and discloses a raise, a
removal, or inheriting a higher fleet value as widening, naming the App
installation's shared bucket and github.agent_reserve_floor.
- The GitHub API budget panel lists each agent's cap, its source and its calls
in the current hour (ghRateLimits.agent_caps, from a new read-only proxy
accessor); config_effective_read shows both keys' values and each agent's
effective cap.
- The two settings-parity allowlist entries are removed, and the operator,
admin MCP and API reference docs describe the controls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BQYnPGFJx81c8JXR3o1sg1
Signed-off-by: Danathar <Danathar@users.noreply.github.com>
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
Open PR overlap checkBase branch: Conflicts are checked with every PR merged onto the current tip of No conflicting open PRs or same-file overlaps were found. |
|
@Danathar this PR is back in your court ( Review asks:
Reply here or push a new head to this branch (same sign-off); CI and the review swarm re-run automatically and the PR returns to the normal path. Queued in your relay fix queue (1 waiting): fixes are handed out before new work as soon as you have capacity. |
…p-controls Signed-off-by: Douglas Baggett <doug.baggett@gmail.com> # Conflicts: # src/docs/operator-reference/runtime-and-quota.md
There was a problem hiding this comment.
Correctness review (head f55e50d)
1. Fleet cap set from the dashboard does not survive a Kubernetes reload/restart — the hub-managed case the PR is for.
PUT /api/config/github → applyFleetAgentGitHubCap (src/pkg/dashboard/api_github_api_cap.go:44-48) sets cfg.AgentsGitHubAPIHourlyCap{,Set} on the live config, and Config.Save() does marshal it into /data/hive.yaml.dashboard (src/pkg/config/github_api_budget.go, marshalConfigYAMLWithAgentCap). But the overlay reader adopts only a fixed set of fields: loadWithDashboardOverlay (src/pkg/config/load.go:149-248) copies OTel/Tracing, Governor.WorkSource, operator cadences, RemovedAgents and Agents from the overlay and nothing else. overlay.AgentsGitHubAPIHourlyCap is never read, so on a pod restart or a ConfigMap-remount reload (the fsnotify watcher goes through LoadWithDashboardOverlay, see save.go:117-125) the fleet cap reverts to the hub's seed. In Kubernetes "the ConfigMap seed plus the overlay is authoritative" (save.go:164), so the hive.yaml write does not rescue it either.
The per-agent override is fine — overlay.Agents entries replace the base wholesale (agent_overlay.go:204-228), so agents.reviewer.github_api_hourly_cap: 600 survives. Only the new fleet control is affected. The PR body and changelog say the save "is saved to … the dashboard overlay" (true) and that this lets a hub-managed spoke change agents.github_api_hourly_cap without hive.yaml (not durably). TestFleetAgentGitHubCapSavesAuditsAndAppliesLive reads the overlay bytes back (github_api_cap_settings_test.go, overlay assertions around the 600 case) but never reloads through LoadWithDashboardOverlay, so it cannot catch this.
Suggested fix, before the fullness guard in load.go (alongside the WorkSource adoption), plus a reload assertion in the test:
if !overlay.Governor.WorkSource.IsZero() {
cfg.Governor.WorkSource = overlay.Governor.WorkSource
}
+ // agents.github_api_hourly_cap is a dashboard-editable fleet key; the
+ // overlay is the only layer the dashboard can write on a hosted spoke.
+ if overlay.AgentsGitHubAPIHourlyCapSet {
+ cfg.AgentsGitHubAPIHourlyCap = overlay.AgentsGitHubAPIHourlyCap
+ cfg.AgentsGitHubAPIHourlyCapSet = true
+ }(Adjacent, pre-existing and out of scope here: github.agent_reserve_floor, edited on the same tab, has the same gap — overlay.GitHub is not adopted at load.go:174-248.)
Everything else I checked holds up: the proxy view (agent_github_budget_view.go:22-46) uses the same now/window as check/pruneLocked and takes the same mutex; handleAgentConfigGeneral writes agentCfg back and calls ExpandAgentReplicas (api_agent_config.go:871-877) so replicas pick up the base's cap; AgentGitHubAPIHourlyCap handles a nil *Config, so buildAgentGitHubCaps with cfg == nil is safe.
— hive: agent=adjudicator backend=copilot model=claude-fable-5.1 copilot=1.0.88
Review follow-ups (below
|
There was a problem hiding this comment.
Read the diff against the v6 tree, including the callers the new code leans on. This looks correct to me; nothing blocking found.
What I verified:
- Replicas follow the base at runtime.
ExpandAgentReplicascopies the wholeAgentConfig(pkg/config/config.go:373), so a replica carries its ownGitHubAPIHourlyCapandAgentGitHubAPIHourlyCap()(pkg/config/github_api_budget.go:160) would read the stale copy first — buthandleAgentConfigGeneralalready re-runsExpandAgentReplicas()after the field write (pkg/dashboard/api_agent_config.go:847), so the copies are rebuilt on the same save.TestAgentGitHubCapGeneralSaveInheritZeroAndReplicaspinsscanner-2at 600 after the PUT, which is the right assertion for this path. nullon the fleet key really removes it.SetFleetAgentGitHubAPIHourlyCap(nil)clearsAgentsGitHubAPIHourlyCapSet, andmarshalConfigYAMLWithAgentCaponly emitsagents.github_api_hourly_capwhen that flag is set (pkg/config/github_api_budget.go:88), so a save after "back to default" writes no key rather than a literal300.- Proxy read is non-mutating.
callsInWindow(crux/proxy/agent_github_budget_view.go) only readsb.eventsunder the lock and skips stale entries without pruning, so the Debug panel cannot change what the next cap check sees. The test cross-checks the count againstb.check. - Admin MCP wiring.
githubCapWriteOps()is appended inDefaultWriteOps;optionalIntArg/requiredStringexist in the package; widening logic (githubCapWidens) treats 0 as uncapped on both sides, matching the getter's semantics. - Changelog fragment is a single
-bullet;diag_config_value_keys.txtadditions are non-secret integers.
One non-blocking note: readFleetGitHubCap in the MCP preview reads settings.agent_github_api_hourly_cap, which forgeGitHubSettingsFor fills with FleetAgentGitHubAPIHourlyCap() (300 when unset), so "default → 600" is disclosed as widening from 300 rather than from "default"; that is the correct comparison, just noting the wording is intentional.
CI had not reported when I read this (tide pending, Go jobs queued), so this is a tree read, not a test result.
— hive: agent=reviewer backend=copilot model=claude-fable-5.1 copilot=1.0.88
|
Hive status — updated in place; edits do not notify. Full history is in the Activity Monitor. Review findings from Read the diff against
CI was still pending when I read it; I have not run anything locally per AGENTS.md. Evidence bundle `#173859715e04` sha256:8f045cae624d6cdfcee0ada08729155ca9a4399ee14e412b07781a4825ff44d8 — download from the dashboard or `hivectl review evidence #17385`. |
…17403) * 🐛 [v6] contribute: never dispatch rework for a merged or closed PR #17385 merged at 13:13:58Z; at ~14:25Z the hub still handed Danathar's relay "fix-pr hive#17385", and the relay ran it twice. Hub: - Queue a nudged PR under owner/name, not the nudge's Display (the bare configured name on a hive that lists repos without the org). Resolve keys on owner/name, so the bare entry was never removed when the PR left stage 072, and the relay could not match the task's repo "hive" to an owner/name. - Before dispatching a queued fix, check its PR is still open (state cache first, else one PR read tagged hive:contribute_fix_dispatch). Merged or closed PRs leave the queue with a log line; an unreadable state holds the dispatch for this tick. fixqueue.Next previews what a dispatch would take so only those PRs are checked. - Send a boundary fix task once: the ready handler ships the returned message, so sendFixTask no longer pushes it as well. - Progress for an in-flight fix task (a boundary fix runs as the relay's primary task) is not a resume claim and is no longer revoked. Relay: - A fix-pr task_id the relay already runs (primary, parallel, preempting or deferred) is ignored on a second task_assign, with a log line naming where it runs; a fix that becomes the primary task says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BQYnPGFJx81c8JXR3o1sg1 Signed-off-by: Danathar <Danathar@users.noreply.github.com> * test(contribute): a retried boundary fix's progress is not revoked (utah-packages#972) projectbluefin/utah-packages#972, 2026-11-09: the relay refused fix-karibhou-1, the hub requeued it and served it at the next ready as fix-karibhou-2 on the same connection, and the relay's progress for the retry (repo empty, client_gen 1870) was answered with "no matching server-issued lease" and task_revoke at 14:50:21Z and 14:54:36Z. The handleTaskProgress change in this PR already covers it; this test drives the real fail -> requeue -> boundary dispatch path and sends progress without a repo, as the relay did, so the retried task id must be tracked on the connection and never revoked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BQYnPGFJx81c8JXR3o1sg1 Signed-off-by: Danathar <Danathar@users.noreply.github.com> * contribute: an unreadable PR state no longer keeps the relay idle When a queued fix's PR state could not be read, selectFixTask answered task_unavailable/fix_pr_in_flight, so the relay got no new issue either. That happens when GitHub is struggling or the API budget is low, the time the hive needs its contributors most. The fix now stays queued and is retried on the next ready, and the relay takes ordinary work meanwhile. Busy fix slots still answer task_unavailable as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BQYnPGFJx81c8JXR3o1sg1 Signed-off-by: Douglas Baggett <doug.baggett@gmail.com> --------- Signed-off-by: Danathar <Danathar@users.noreply.github.com> Signed-off-by: Douglas Baggett <doug.baggett@gmail.com> Co-authored-by: Danathar <Danathar@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: hivecommons-hive[bot] <280983584+hivecommons-hive[bot]@users.noreply.github.com>
Summary
The API proxy's per-agent GitHub API hourly cap can now be changed without editing
hive.yaml. Settings → Forge App gets a fleet default control next to the agent reserve floor, each agent's General tab gets its own value, and the admin MCP gets two matching write operations. A save applies to the proxy at once (no restart), is saved tohive.yamland the dashboard overlay, and is audited with the old and new value. The GitHub API budget panel now lists each agent's cap and its calls in the current hour.Why
On a hub-managed spoke the hub owns
hive.yaml, so the spoke owner had no way to changeagents.github_api_hourly_caporagents.<name>.github_api_hourly_cap. The projectbluefin hive wants the fleet at 300 and the reviewer at 600: the reviewer hits its 300/h cap overnight, and the reviews it writes after that come back as false "requires human".What changes
agents.github_api_hourly_cap,0removes the cap, a blank field removes the key so the default 300 applies. It saves throughPUT /api/config/githubasagent_github_api_hourly_cap(null= back to the default), the same path and audit (config_github) as the reserve floor.0for no cap, blank to inherit. It saves throughPUT /api/config/agent/{name}/generalasgithubApiHourlyCap(null= inherit), audited asconfig_agent_general. Replicas and review heads are copies of their base agent, so their tab shows the inherited value read-only and the endpoint refuses an override on them (400 naming the base agent). The save rebuilds the copies at once, so they follow the base straight away.main.gohands the proxyb.cfg.AgentGitHubAPIHourlyCapbound to the live*Config, and the dashboard'sConfigis that same pointer, so both saves change what the proxy enforces on its next request. Tests pin this through the bound method value.agent.github_api_hourly_cap(agent,cap: number orinherit) andgithub.agent_api_hourly_cap(cap: number ordefault), throughwrite_preview/write_confirm. The preview reads the current value: raising a cap, removing one, or inheriting a higher fleet value is disclosed as widening, naming the App installation's shared bucket andgithub.agent_reserve_floor; lowering is not. If the current value cannot be read, the preview says the change may be widening.agent_github_budget_view.go).config_effective_readshows both keys' values (added to the non-secret value list) and a newagent_github_api_hourly_capsblock with each agent's effective cap.TODO #11854allowlist lines for these keys are removed; both controls carry a YAML hint and asettingHelpMark.Not changed: the 300 default, the reserve floor value, the App's bucket, and the cap getter itself (Refs #17379 covers the getter's fallback). Kept clear of #17376 and #17378, which edit
agent_github_budget.go.Files
api_config.go,api_agent_config.go,forge_apps.go,status_builder.go,api_diag_config.go, newapi_github_api_cap.go,github_api_cap_view.go,diag_github_api_caps.go;static-src/js/{config-tabs,config-agent,debug-logs,shimmy-handlers}.jsand the regeneratedstatic/index.html;settings_parity_allowlist.txt,diag_config_value_keys.txt.pkg/config/github_api_cap_settings.go(fleet getter and setter).crux/proxy/agent_github_budget_view.go;cmd/hive/main.gowires it to the dashboard.pkg/adminmcp/tools_github_cap_ops.go, registered intools_agent_ops.go.Tests
None run locally (repo AGENTS.md); CI is the verdict. New tests:
TestFleetAgentGitHubCapSavesAuditsAndAppliesLive: bad values are refused; 600, then 0, thennulleach change the cap the proxy-bound getter returns, the overlay, the forge-apps answer and the audit detail.TestAgentGitHubCapGeneralSaveInheritZeroAndReplicas: the override reaches the agent and its replica at once, lands in the overlay and the agent file,0is kept,nullclears it, a replica is refused, and an unrelated save leaves the cap alone.TestGHRateLimitsCarryAgentCaps,TestDiagConfigShowsAgentGitHubCaps,TestGitHubCapControlsRender,TestAgentGitHubCallsLastHourMatchesTheCapWindow.On the current v6 tip each of these fails: the two endpoints ignore the new fields (so the getter keeps returning 300), the response fields, the agent caps table, the proxy accessor and the controls do not exist, and the two write ops are not registered.
🤖 Generated with Claude Code
https://claude.ai/code/session_01BQYnPGFJx81c8JXR3o1sg1